package model.impl;

import java.util.LinkedList;
import java.util.List;
import java.util.Queue;

import sharedGameLogic.enums.BattleDistance;
import sharedGameLogic.enums.BattleEvent;
import sharedGameLogic.enums.GameStatus;
import sharedGameLogic.gameData.BattleResult;
import sharedGameLogic.gameData.InitialPlayerStatus;
import sharedGameLogic.gameData.InitialStatus;
import sharedGameLogic.gameData.PlayerResult;
import sharedGameLogic.players.BattleStatus;
import sharedGameLogic.players.Player;

import model.IModelCreator;
import model.IServerModel;
import model.IServerNetwork;
import model.impl.actionModifiers.*;
import network.ModelListener;
import network.NetworkID;

/**
 * This class models a game of StreetJanken.
 * 
 * @author Tobias
 *
 */
public class Model implements IServerModel, IServerNetwork, IModelCreator {
	//players
	private Player player1;
	private Player player2;
	
	private NetworkID player1ID;
	private NetworkID player2ID;
	
	//Queues of actions the players are about to do.
	private Queue<BattleAction> player1Actions;
	private Queue<BattleAction> player2Actions;
	
	//Has the model received new instructions from the users.
	private boolean isPlayer1Updated;
	private boolean isPlayer2Updated;
	
	//The results from the last round
	private Queue<BattleResult> results;
	
	private Object sendObject;
	
	//If the battle is currently in closed or distances combat
	private BattleDistance battleDistance;
	
	//A list of all listeners...
	private List<ModelListener> listeners;
	
	private static final float ENERGY_COST_COEFFICIENT = 0.1F; 
	
	/*
	 * The class used to compare actions in the game. A BattleAction
	 * object is converted into a ActionComparatorData object that 
	 * contains the values:
	 * 
	 * - damage: how much damage the attack will do to the opponent.
	 * - hitValue: how likely it is that the attack hits the opponent
	 * - block: how much damage that will be reduced from the 
	 * 			opponents attack (if you are hit)
	 * - avoidValue: how likely it is that you will avoid the 
	 * 				 opponents attack
	 */
	static class ActionComparatorData{
		private int damage;
		private int hitValue;
		private int block;
		private int avoidValue;
		
		public ActionComparatorData(){
		}
		
		public ActionComparatorData(int dam, int hit, int blo, int avo){
			damage = dam;
			hitValue = hit;
			block = blo;
			avoidValue = avo;
		}
		
		public int getDamage(){
			return damage;
		}
		public int getHitValue(){
			return hitValue;
		}
		public int getBlock(){
			return block;
		}
		public int getAvoidValue(){
			return avoidValue;
		}

		public void setDamage(int dam){
			damage = dam;
		}
		public void setHitValue(int hit){
			hitValue = hit;
		}
		public void setBlock(int blo){
			block = blo;
		}
		public void setAvoidValue(int avo){
			avoidValue = avo;
		}
	}
	
	/**
	 * Static methods that creates and returns a Model
	 * object as an IModelCreator. 
	 * 
	 * @return - A new Model as an IModelCreator
	 */
	public static IModelCreator newInstance(){
		return new Model();
	}
	
	/*
	 * Private constructor for Model. Initiates necessary
	 * variables. Use newInstance() to get a Model (IModelCreator) 
	 * object.
	 */
	private Model() {
		listeners = new LinkedList<ModelListener>();
		results = new LinkedList<BattleResult>();
		battleDistance = BattleDistance.CLOSE_COMBAT;
		isPlayer1Updated = false;
		isPlayer2Updated = false;
		player1Actions = new LinkedList<BattleAction>();
		player2Actions = new LinkedList<BattleAction>();
	}
	
	@Override
	public void update(NetworkID playerID, Queue<BattleEvent> queue){
		if(playerID == player1ID){
			updatePlayer(player1, queue);
		}
		else if(playerID == player2ID){
			updatePlayer(player2, queue);
		}
		else{
			throw new IllegalArgumentException("PlayerID does not exist");
		}
		
		//If the model has got all the inputs it needs it will run one round of the game
		if(isPlayersReady()){
			runGameRound();
		}
	}
	
	/*
	 * Updates a player object with 'instructions' from the actual user of a client
	 */
	private void updatePlayer(Player player, Queue<BattleEvent> queue){
		if(player == player1){
			if(!isPlayer1Updated){
				player1Actions = convertQueuedActions(player.getCharacter().getBattleStatus(), queue);
				isPlayer1Updated = true;
			}
			else{
				throw new IllegalArgumentException("Player 1 is already updated");
			}
		}
		else if(player == player2){
			if(!isPlayer2Updated){
				player2Actions = convertQueuedActions(player.getCharacter().getBattleStatus(), queue);
				isPlayer2Updated = true;
			}
			else{
				throw new IllegalArgumentException("Player 2 is already updated");
			}
		}
		else{
			throw new IllegalArgumentException("Player does not exist in this game");
		}
	}
	
	/*
	 * Runs one round of the game.
	 */
	private void runGameRound(){
		results.clear();
		calcResults();
		sendObject = results;
		changed();
		isPlayer1Updated = false;
		isPlayer2Updated = false;
		
		if(isGameOver()){
			resetModel();
		}
	}
	
	/*
	 * Calculates and sets the results from all inputed actions.
	 */
	private void calcResults() {
		if(player1Actions.size() != player2Actions.size()){
			throw new IllegalArgumentException("Amount of actions does not match.");
		}	
		for(int i = 0; i < player1Actions.size(); i++){
			results.add(compareActions(player1Actions.poll(), player2Actions.poll()));
		}
	}

	/*
	 * Converts a Queue of BattleActionType enums to a Queue of BattleActions using
	 * 'convertAction(...)'
	 */
	private Queue<BattleAction> convertQueuedActions(BattleStatus status, Queue<BattleEvent> type){
		Queue<BattleAction> returnQueue = new LinkedList<BattleAction>();
		for(int i = 0; i < type.size(); i++){
			returnQueue.add(convertAction(status, type.poll()));
		}
		return returnQueue;
	}
	
	/*
	 * Creates one BattleAction object from a BattleActionType enum ('type') 
	 * that decides what kind of action that is going to be made and a 
	 * Characters BattleStatus ('status') that decides how effectice the 
	 * action will be.
	 */
	private BattleAction convertAction(BattleStatus status, BattleEvent type){
		ActionProducer actionProducer = new ActionProducer();
		
		switch (type){
			//Physical attacks (close combat)
			case STRONG_ATTACK:
				actionProducer.setActionBuilder(new StrongAttackBuilder());
				break;
			case FAST_ATTACK:
				actionProducer.setActionBuilder(new FastAttackBuilder());
				break;
			case PRECISE_ATTACK:
				actionProducer.setActionBuilder(new PreciseAttackBuilder());
				break;
			
			//Physical attacks (ranged)
			case RANGED_STRONG_ATTACK:
				actionProducer.setActionBuilder(new RangedStrongAttackBuilder());
				break;
			case RANGED_FAST_ATTACK:
				actionProducer.setActionBuilder(new RangedFastAttackBuilder());
				break;
			case RANGED_PRECISE_ATTACK:
				actionProducer.setActionBuilder(new RangedPreciseAttackBuilder());
				break;
			
			//Defensive actions
			case DODGE:
				actionProducer.setActionBuilder(new DodgeBuilder());
				break;
			case BLOCK:
				actionProducer.setActionBuilder(new BlockBuilder());
				break;
			
			//'Strategic' actions ('Counter', 'Retreat' is close combat. 'Charge' is ranged)
			case COUNTER:
				actionProducer.setActionBuilder(new CounterBuilder());
				break;
			case CHARGE:
				actionProducer.setActionBuilder(new ChargeBuilder());
				break;
			case RETREAT:
				actionProducer.setActionBuilder(new RetreatBuilder());
				break;
				
			//Elemental magical attacks (ranged & close combat)
			case MAGIC_FIRE:
				actionProducer.setActionBuilder(new MagicFireBuilder());
				break;
			case MAGIC_WATER:
				actionProducer.setActionBuilder(new MagicWaterBuilder());
				break;
			case MAGIC_WIND:
				actionProducer.setActionBuilder(new MagicWindBuilder());
				break;
			case MAGIC_WOOD:
				actionProducer.setActionBuilder(new MagicWoodBuilder());
				break;
			
			//Combined magical/physical attacks (close combat)
			case MAGIC_STRONG_ATTACK:
				actionProducer.setActionBuilder(new MagicStrongAttackBuilder());
				break;
			case MAGIC_FAST_ATTACK:
				actionProducer.setActionBuilder(new MagicFastAttackBuilder());
				break;
			case MAGIC_PRECISE_ATTACK:
				actionProducer.setActionBuilder(new MagicPreciseAttackBuilder());
				break;
		}
		
		actionProducer.constructBattleAction(status);

		return actionProducer.getBattleAction();
	}
	
	/*
	 * Checks if the player objects are ready to run a new game round.
	 */
	private boolean isPlayersReady() {
		return isPlayer1Updated && isPlayer2Updated && (player1 != null && player2 != null);
	}
	
	/*
	 * Calculates one BattleResult object from two actions.
	 */
	private BattleResult compareActions(BattleAction action1, BattleAction action2){
		ActionComparatorData player1Comp = setActionData(action1);
		ActionComparatorData player2Comp = setActionData(action2);
		
		int damageDone1=0;
		int damageDone2=0;
		
		boolean didHit1 = false;
		boolean didHit2 = false;
		
		//The energy cost "logic" is below
		
		int energyCost1=0;
		int energyCost2=0;
		
		energyCost1 = Math.round(action1.getMagicalStrength()
				*ENERGY_COST_COEFFICIENT);
		energyCost2 = Math.round(action2.getMagicalStrength()
				*ENERGY_COST_COEFFICIENT);
		
		//If energy cost is larger than player's energy...
		//Lowers the attacks effect (hitValue & damage)
		//Sets player energy = 0
		if(energyCost1 > player1.getCharacter().getBattleStatus().getEnergy()){
			float attackEffect = (float)player1.getCharacter().getBattleStatus().getEnergy()/energyCost1;
			player1Comp = setActionData(action1, attackEffect);
			energyCost1 = player1.getCharacter().getBattleStatus().getEnergy();
		}
		if(energyCost2 > player2.getCharacter().getBattleStatus().getEnergy()){
			float attackEffect = (float)player2.getCharacter().getBattleStatus().getEnergy()/energyCost2;
			player2Comp = setActionData(action2, attackEffect);
			energyCost2 = player2.getCharacter().getBattleStatus().getEnergy();
		}
		
		player1.getCharacter().getBattleStatus().setEnergy(
				player1.getCharacter().getBattleStatus().getEnergy()
				-energyCost1);
		player2.getCharacter().getBattleStatus().setEnergy(
				player1.getCharacter().getBattleStatus().getEnergy()
				-energyCost2);
		
		//This is the logic to determine if an action hits, 
		//and how much damage it inflicts if it does. 
		
		if(player1Comp.getHitValue() > player2Comp.getAvoidValue()){
			if(player1Comp.getDamage() > player2Comp.getBlock()){
				damageDone1 = player1Comp.getDamage() - player2Comp.getBlock();
			}
			else{
				damageDone1 = 0;
			}
			player2.getCharacter().getBattleStatus().setHealth(
					player2.getCharacter().getBattleStatus().getHealth()
					-damageDone1);
			didHit1 = true;
		}
		
		if(player2Comp.getHitValue() > player1Comp.getAvoidValue()){
			if(player2Comp.getDamage() > player1Comp.getBlock()){
				damageDone2 = player2Comp.getDamage() - player1Comp.getBlock();
			}
			else{
				damageDone2 = 0;
			}
			player1.getCharacter().getBattleStatus().setHealth(
					player1.getCharacter().getBattleStatus().getHealth()
					-damageDone2);
			didHit2 = true;
		}
		
		//Setting PlayerResult objects...
		PlayerResult result1 = new PlayerResult(
				player1.getCharacter().getBattleStatus().getHealth(), 
				player1.getCharacter().getBattleStatus().getEnergy(), 
				action1.getBattleActionType(), didHit1);
		
		PlayerResult result2 = new PlayerResult(
				player2.getCharacter().getBattleStatus().getHealth(), 
				player2.getCharacter().getBattleStatus().getEnergy(), 
				action2.getBattleActionType(), didHit2);
		
		//Calculating new BattleDistance
		if(battleDistance == BattleDistance.CLOSE_COMBAT){
			if(action1.getBattleActionType() == BattleEvent.RETREAT || 
					action2.getBattleActionType() == BattleEvent.RETREAT){
				battleDistance = BattleDistance.DISTANCED_COMBAT;
			}	
		}
		else{
			if(action1.getBattleActionType() == BattleEvent.CHARGE || 
					action2.getBattleActionType() == BattleEvent.CHARGE){
				battleDistance = BattleDistance.CLOSE_COMBAT;
			}
		}
		
		return new BattleResult(result1, result2, battleDistance, getGameStatus());
	}
	
	/*
	 * Calculates one ActionComparatorData object from one BattleAction object.
	 */
	private ActionComparatorData setActionData(BattleAction action, float attackEffect){
		ActionComparatorData actionComp = new ActionComparatorData();
		
		actionComp.setHitValue(Math.round(((float)
								(action.getAttackPrecision() + 
								action.getAttackSpeed() + 
								action.getMagicalPrecision()) / 3) * attackEffect));
		
		actionComp.setDamage(Math.round(((float)
								(action.getAttackStrength() + 
								action.getAttackSpeed() + 
								action.getAttackPrecision() + 
								action.getMagicalStrength() + 
								action.getAttackPrecision())/5) * attackEffect));
		
		actionComp.setBlock(action.getBlock());
		
		actionComp.setAvoidValue(action.getDodge());
		
		return actionComp;
	}
	
	/*
	 * setActionData(...) with attackEffect set as standard value, 1. 
	 * attackEffect is lower if player doesn't have enough energy for the
	 * attack.
	 */
	private ActionComparatorData setActionData(BattleAction action){
		return setActionData(action, 1);
	}
	
	/*
	 * Resets the model so that it can be initialized again and a new
	 * game can be run.
	 */
	private void resetModel(){
		player1 = null;
		player2 = null;
		
		sendObject = null;
		results = new LinkedList<BattleResult>();
		battleDistance = BattleDistance.CLOSE_COMBAT;
		isPlayer1Updated = false;
		isPlayer2Updated = false;
		player1Actions = new LinkedList<BattleAction>();
		player2Actions = new LinkedList<BattleAction>();
	}
	
	/*
	 * Returns the current status of the game considering
	 * if game is still going on, if the game is over and
	 * in that case; who won the game.
	 */
	private GameStatus getGameStatus() {
		if(!isGameOver()){
			return GameStatus.FIGHT_GOING_ON;
		}
		else{
			if(player1.getCharacter().getBattleStatus().getHealth() <= 0 && 
					player2.getCharacter().getBattleStatus().getHealth() <= 0){
				return GameStatus.DRAW;
			}
			else if(player1.getCharacter().getBattleStatus().getHealth() <= 0){
				return GameStatus.PLAYER_TWO_WON;
			}
			else{
				return GameStatus.PLAYER_ONE_WON;
			}
		}
	}
	
	@Override
	public void init(Player player, NetworkID id) {
		if(player1 == null){
			player1 = (Player)player.clone();
			player1ID = id;
		}
		else if(player2 == null){
			if(id == player1ID){
				throw new IllegalArgumentException("ID already used");
			}
			player2 = (Player)player.clone();
			player2ID = id;
			
			//When both players are initialized
			results.clear();
			player1Actions.clear();
			player2Actions.clear();
			
			player1.getCharacter().setBasicBattleStatus();
			player2.getCharacter().setBasicBattleStatus();
			
			sendObject = getInitialStatus(player1, player2);
			changed();
		}
		//Else: do nothing.
	}
	
	/*
	 * Calculates an InitialStatus object that should be send to the clients
	 * in order to inform clients about players max hp, max energy and names
	 * before the game starts.
	 */
	private InitialStatus getInitialStatus(Player player1, Player player2){
		InitialPlayerStatus p1Stats;
		p1Stats = new InitialPlayerStatus(player1.getPlayerName(),
										  player1.getCharacter().getName(),
										  player1.getCharacter().getBattleStatus().getHealth(),
										  player1.getCharacter().getBattleStatus().getEnergy());
		
		InitialPlayerStatus p2Stats;
		p2Stats = new InitialPlayerStatus(player2.getPlayerName(),
										  player2.getCharacter().getName(),
										  player2.getCharacter().getBattleStatus().getHealth(),
										  player2.getCharacter().getBattleStatus().getEnergy());
		
		return new InitialStatus(p1Stats, p2Stats, battleDistance);
	}

	@Override
	public boolean isGameOver() {
		if(player1.getCharacter().getBattleStatus().getHealth() <= 0){
			return true;
		}
		else if(player2.getCharacter().getBattleStatus().getHealth() <= 0){
			return true;
		}
		return false;
	}

	@Override
	public Object getData() {
		return sendObject;
	}
	
	/*
	 * Notifies all listeners that the state of the model has been changed.
	 */
	private void changed() {
		for(ModelListener listener : listeners) {
			listener.modelChanged();
		}
	}

	@Override
	public void addModelListener(ModelListener listener) {
		listeners.add(listener);
	}

	@Override
	public boolean removeModelListener(ModelListener listener) {
		return listeners.remove(listener);
	}

	@Override
	public boolean isGameReady() {
		return player1 != null && player2 != null;
	}

	@Override
	public IServerModel getAsIServerModel() {
		return this;
	}

	@Override
	public IServerNetwork getAsIServerNetwork() {
		return this;
	}
}